﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Diagnostics.Contracts;
using System.Runtime.InteropServices;
using Adler32Checksum;
using System.IO;
using System.Globalization;
using System.Threading;

namespace RPGMapRevealer
{
    internal static class TileAnalyser
    {
        public static TileCollection AnalyzeWholeImage(Bitmap publicLayer, Bitmap secretLayer, MapInfo mapInfo, DirectoryInfo saveTiles = null)
        {
            IAsyncResult result = BeginAnalyzeWholeImage(publicLayer, secretLayer, mapInfo, saveTiles);
            return EndAnalyzeWholeImage(result);
        }
        public static AsyncAnalysisReport BeginAnalyzeWholeImage(Bitmap publicLayer, Bitmap secretLayer, MapInfo mapInfo, DirectoryInfo saveTiles = null)
        {
            AsyncAnalysisReport result = new AsyncAnalysisReport();
            result.publicLayer = publicLayer;
            result.secretLayer = secretLayer;
            result.mapInfo = mapInfo;
            result.saveTiles = saveTiles;
            ThreadPool.QueueUserWorkItem(new WaitCallback(DoAnalyzeWholeImage), result);
            return result;
        }
        public static TileCollection EndAnalyzeWholeImage(IAsyncResult result)
        {
            AsyncAnalysisReport castedResult = (AsyncAnalysisReport)result;
            castedResult.AsyncWaitHandle.WaitOne();
            return castedResult.Result;
        }

        /**
         * Analyzes all the tiles in an image
         */
        private static void DoAnalyzeWholeImage(Object state)
        {
            AsyncAnalysisReport asyncresult = (AsyncAnalysisReport)state;
            Bitmap publicLayer = asyncresult.publicLayer;
            Bitmap secretLayer = asyncresult.secretLayer;
            MapInfo mapInfo = asyncresult.mapInfo;
            DirectoryInfo saveTiles = asyncresult.saveTiles;
            Contract.Requires(publicLayer != null);
            Contract.Requires(secretLayer == null || secretLayer.Height == publicLayer.Height);
            Contract.Requires(secretLayer == null || secretLayer.Width == publicLayer.Width);
            Contract.Requires(mapInfo != null);
            GridConfig grid = mapInfo.GridConfig;
            int numTilesX = (publicLayer.Width - grid.TileOffsetX) / grid.TileWidth;
            int numTilesY = (publicLayer.Height - grid.TileOffsetY) / grid.TileHeight;
            int ox = grid.TileOffsetX;
            int oy = grid.TileOffsetY;
            int tx = grid.TileWidth;
            int ty = grid.TileHeight;
            int minX = numTilesX;
            int minY = numTilesY;
            int maxX = 0;
            int maxY = 0;

            asyncresult.MaxTiles = numTilesX * numTilesY;
            asyncresult.timeStarted = DateTime.Now;

            //break image into tiles
            Dictionary<String, Tile> temp = new Dictionary<string, Tile>();
            TileCollection result = mapInfo.Tiles;
            result.Clear();
            for (int x = 0; x < numTilesX; x++)
            {
                for (int y = 0; y < numTilesY; y++)
                {
                    int x1 = (x * tx) + ox;
                    int y1 = (y * ty) + oy;

                    Rectangle tileArea = new Rectangle(x1, y1, tx, ty);
                    if (tileArea.Top < 0 || tileArea.Left < 0 || tileArea.Width <= 0 || tileArea.Height <= 0)
                    {
                        throw new CodeContractsException("This should never have happened since x and y are supposed to be positive, and the width and hieght were already checked at the start of the function");
                    }

                    Tile tile = AnalyzeImageTile(publicLayer, secretLayer, tileArea, saveTiles, grid.TileBorder);
                    asyncresult.TilesSearched++;
                    if (!tile.IsAlwaysEmpty)
                    {
                        asyncresult.TilesFound++;
                        tile.X = x;
                        tile.Y = y;
                        tile.Map = mapInfo;
                        result.Add(tile);
                        minX = Math.Min(minX, x);
                        maxX = Math.Max(maxX, x);
                        minY = Math.Min(minY, y);
                        maxY = Math.Max(maxY, y);
                        temp.Add(key(x, y), tile);
                    }
                }
            }

            if (minX >= maxX || minY >= maxY)
            {
                throw new CodeContractsException("This should never happen because I trust Math.Min and Math.Max to work");
            }

            result.MinX = minX;
            result.MinY = minY;
            result.MaxX = maxX;
            result.MaxY = maxY;

            //link adjacent tiles
            Tile current = null;
            Tile next = null;
            for (int x = minX; x <= maxX; x++)
            {
                for (int y = minY; y <= maxY; y++)
                {
                    if (temp.TryGetValue(key(x, y), out current))
                    {
                        if (current == null) { continue; }
                        if (temp.TryGetValue(key(x + 1, y), out next))
                        { current.AdjacentTiles.Add(next); }
                        if (temp.TryGetValue(key(x, y + 1), out next))
                        { current.AdjacentTiles.Add(next); }
                        if (temp.TryGetValue(key(x - 1, y), out next))
                        { current.AdjacentTiles.Add(next); }
                        if (temp.TryGetValue(key(x, y - 1), out next))
                        { current.AdjacentTiles.Add(next); }
                    }
                }
            }
            asyncresult.Result = result;
        }
        private static String key(int x, int y)
        { return x + "|" + y; }

        /**
         * Analyzes an section of an image to determine it's properties.
         * Specifcally if it is empty, if the two layers differ, and it generates a name that can be used to sufficiently uniquely identify a tile
         */
        private static Tile AnalyzeImageTile(Bitmap publicLayer, Bitmap secretLayer, Rectangle rect, DirectoryInfo saveTiles, int borderWidth)
        {
            Contract.Requires(publicLayer != null);
            Contract.Requires(rect.Width > 0);
            Contract.Requires(rect.Height > 0);
            Contract.Requires(rect.Left >= 0);
            Contract.Requires(rect.Top >= 0);

            Rectangle innerRect = new Rectangle(rect.X + borderWidth, rect.Y + borderWidth, rect.Width - borderWidth * 2, rect.Height - borderWidth * 2);
            if (innerRect.Left < 0 || innerRect.Top < 0)
            { throw new CodeContractsException(); }
            Tile result = new Tile();

            byte[] publicBytes = null;
            byte[] secretBytes = null;

            publicBytes = getBytes(publicLayer, innerRect);
            if (secretLayer != null && secretLayer != publicLayer)
            { secretBytes = getBytes(secretLayer, innerRect); }
            else
            { secretBytes = publicBytes; }

            if (publicBytes == null)
            { throw new CodeContractsException(); }
            int size = publicBytes.Length;

            bool PAllZero = true;
            bool PAllFF = true;
            bool SAllZero = true;
            bool SAllFF = true;
            bool AllSame = true;

            for (int i = 0; i < size; i++)
            {
                if (publicBytes[i] != 0) { PAllZero = false; }
                if (publicBytes[i] != 255) { PAllFF = false; }
            }
            if (secretBytes != null)
            {
                if (size > secretBytes.Length)
                { throw new CodeContractsException(); }
                for (int i = 0; i < size; i++)
                {
                    if (secretBytes[i] != 0) { SAllZero = false; }
                    if (secretBytes[i] != 255) { SAllFF = false; }
                    if (publicBytes[i] != secretBytes[i]) { AllSame = false; }
                }
            }

            //re-fetch image with wider borders
            innerRect = new Rectangle(rect.X + (borderWidth - 1), rect.Y + (borderWidth - 1), rect.Width - (borderWidth - 1) * 2, rect.Height - (borderWidth - 1) * 2);
            publicBytes = getBytes(publicLayer, innerRect);
            if (secretLayer != null && secretLayer != publicLayer)
            { secretBytes = getBytes(secretLayer, innerRect); }
            else
            { secretBytes = publicBytes; }

            String pName = makeName(publicBytes) + ".png";
            String sName = secretBytes != null ? makeName(secretBytes) + ".png" : pName;


            result.HasSecret = !AllSame;
            result.IsEmptyPublic = PAllZero || PAllFF;
            result.IsEmptySecret = SAllZero || SAllFF;

            result.ImageNamePublic = pName;
            result.ImageNameSecret = sName;

            if (saveTiles != null && !result.IsAlwaysEmpty)
            {
                String pFileName = Path.Combine(saveTiles.FullName, pName);
                String sFileName = Path.Combine(saveTiles.FullName, sName);
                if (String.IsNullOrWhiteSpace(pFileName) || String.IsNullOrWhiteSpace(sFileName))
                {
                    throw new CodeContractsException("This should never be called because I appended a literal string to both pName and sName");
                }
                SaveTile(publicLayer, rect, pFileName);
                if (!AllSame && secretLayer != null)
                {
                    SaveTile(secretLayer, rect, sFileName);
                }
            }
            return result;
        }

        /// <summary>
        /// This function returns the bytes of the image corresponding to the specified rectangle.
        /// </summary>
        /// <remarks>
        /// The LockBits function is unreliable, sometimes it returns the full stride of the image, sometimes it returns the stride you asked for.  This function copies the image first to guarentee we get the right data.
        /// </remarks>
        /// <param name="img"></param>
        /// <param name="r"></param>
        /// <returns></returns>
        private static byte[] getBytes(Image img, Rectangle r)
        {
            Contract.Requires(img != null);
            Contract.Requires(r.Left >= 0);
            Contract.Requires(r.Top >= 0);
            Contract.Ensures(Contract.Result<byte[]>() != null);
            double bpp = 3;
            double pad = 8;
            int padW = r.Width;
            int padH = r.Height;
            while ((padW * bpp) % pad != 0) { padW++; }
            using (Bitmap temp = new Bitmap(padW, padH, PixelFormat.Format24bppRgb))
            {
                if ((temp.PixelFormat & PixelFormat.Indexed) != 0)
                { throw new CodeContractsException(); }
                using (Graphics tempG = Graphics.FromImage(temp))
                {
                    tempG.DrawImageUnscaled(img, -r.Left, -r.Top);
                    tempG.Flush();
                }
                System.Drawing.Imaging.BitmapData tempData = null;
                try
                {
                    tempData = temp.LockBits(new Rectangle(0, 0, padW, padH), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
                    int size = tempData.Stride * tempData.Height;
                    if (size < 0)
                    { throw new CodeContractsException("getting tired of these ridiculous checks... really wish M$ would add contracts to the rest of it's API"); }
                    byte[] result = new byte[size];
                    System.Runtime.InteropServices.Marshal.Copy(tempData.Scan0, result, 0, size);
                    return result;
                }
                finally
                {
                    if (tempData != null)
                    {
                        temp.UnlockBits(tempData);
                    }
                }
            }
        }

        private static void SaveTile(Bitmap image, Rectangle tileCoordinates, String name)
        {
            Contract.Requires(image != null);
            Contract.Requires(tileCoordinates.Width > 0);
            Contract.Requires(tileCoordinates.Height > 0);
            Contract.Requires(tileCoordinates.Left >= 0);
            Contract.Requires(tileCoordinates.Top >= 0);
            Contract.Requires(name != null);
            Contract.Requires(!String.IsNullOrWhiteSpace(name));
            if (File.Exists(name)) { return; }

            using (Bitmap temp = new Bitmap(tileCoordinates.Width, tileCoordinates.Height, PixelFormat.Format32bppArgb))
            {
                if ((temp.PixelFormat & PixelFormat.Indexed) != 0)
                {
                    throw new BadImageFormatException("This should never be called because I know exactly which image format I'm creating");
                }
                using (Graphics g = Graphics.FromImage(temp))
                {
                    g.DrawImage(image, 0 - tileCoordinates.Left, 0 - tileCoordinates.Top);
                    g.Flush();
                    temp.Save(name, ImageFormat.Png);
                }
            }
        }

        private static String makeName(byte[] data)
        {
            Contract.Requires(data != null);
            return Adler32.Calculate(data).ToString("X", CultureInfo.InvariantCulture).Trim('-');
        }


    }

    public class AsyncAnalysisReport : IAsyncResult
    {
        public Bitmap publicLayer { get; set; }
        public Bitmap secretLayer { get; set; }
        public MapInfo mapInfo { get; set; }
        public DirectoryInfo saveTiles { get; set; }
        public bool UsedThread { get; set; }

        public event EventHandler<EventArgs> CompletedEvent;

        public TileCollection Result
        {
            get { return result; }
            set
            {
                result = value;
                GetWaitHandle().Set();
                if (CompletedEvent != null)
                { CompletedEvent(this, null); }
            }
        }
        private TileCollection result;

        public int TilesFound { get; set; }
        public int TilesSearched { get; set; }
        public int MaxTiles { get; set; }
        public DateTime timeStarted { get; set; }

        private Object state;
        #region IAsyncResult Members

        public object AsyncState
        {
            get { return state; }
        }

        private readonly Object synclock = new Object();
        private ManualResetEvent waitHandle;
        private ManualResetEvent GetWaitHandle()
        {
            lock (synclock)
            {
                if (waitHandle == null)
                { waitHandle = new ManualResetEvent(false); }
                if (result != null)
                { waitHandle.Set(); }
            }
            return waitHandle;
        }

        public System.Threading.WaitHandle AsyncWaitHandle
        { get { return GetWaitHandle(); } }

        public bool CompletedSynchronously
        { get { return !UsedThread; } }

        public bool IsCompleted
        { get { return result != null; } }

        #endregion
    }
}
